import {UploadTask} from "./upload-task.ts";
import type {IRawFile} from "../types.ts";
import type {UploadProgressEvent} from "element-plus";
import type {IMinioFile} from "./types.ts";
import minioApi from '@/api/modules/minio';
import {to} from "sf-utils2";
import {calculatePartSize, calculateFileMD5} from "./util.ts";
import uploadDb from "./upload-db.ts";
import dayjs from "dayjs";

/**
 * 文件上传参数
 */
export interface IUploadOption {
	file: IRawFile,
	module: string,
	md5?: string,
	bucketType?: 'private' | 'public',
	onProgress?: (event: UploadProgressEvent) => void,
	onError?: (evt: Error) => void,
	onSuccess?: (response: { code: number, message: string, data: IMinioFile }) => void,
	onReading?: (event: UploadProgressEvent) => void
}

export class MiniClient {
	/**
	 * 最大文件大小 50GB
	 * @protected
	 */
	protected readonly maxObjectSize = 50 * 1024 * 1024 * 1024;
	/**
	 * 支持分片上传的最大文件大小 50MB
	 * @protected
	 */
	protected readonly supportMultipartSize = 50 * 1024 * 1024;
	/**
	 * 存储桶名称
	 * @protected
	 */
	protected _privateBucketName: string;
	protected _publicBucketName: string;
	/**
	 * 上传任务
	 * @private
	 */
	private uploadTask = new UploadTask();
	/**
	 * 请求列表
	 * @private
	 */
	private _reqs: Record<string, XMLHttpRequest | AbortController> = {};

	/**
	 * 设置存储桶
	 * @param buket
	 */
	setBucket(buket: { privateBucketName: string, publicBucketName: string }) {
		this._privateBucketName = buket.privateBucketName;
		this._publicBucketName = buket.publicBucketName;
	}

	/**
	 * 是否已经初始化
	 */
	get isInitiated() {
		return this._privateBucketName && this._publicBucketName;
	}

	/**
	 * 获取存储桶名称
	 * @param buketType 存储桶类型
	 */
	getBucketName(buketType: 'private' | 'public') {
    // 生产要求，强制只传私有桶
    buketType = 'private';
		return buketType === 'private' ? this._privateBucketName : this._publicBucketName;
	}

	/**
	 * 获取路径
	 * @param module
	 */
	getPath(module: string) {
		return module + dayjs().format('/YYYY-MM/')
	}

	/**
	 * 上传文件，根据文件大小选择单文件上传或分片上传
	 * @param option
	 * @param controller
	 */
	putObject(option: IUploadOption, controller?: AbortController) {
		if (option.file.size >= this.maxObjectSize) {
			throw new Error('文件过大，不支持上传');
		}
		controller ??= new AbortController();
		const {signal} = controller;
		this._reqs[option.file.uid] = controller;

		if (option.file.size <= this.supportMultipartSize) {
			return this.singleUpload(option, signal);
		} else {
			return this.multipartUpload(option, signal);
		}
	}

	/**
	 * 简单文件上传
	 * @param option
	 * @param signal
	 */
	private async singleUpload(option: IUploadOption, signal: AbortSignal) {
		const bucketName = this.getBucketName(option.bucketType);
		const path = this.getPath(option.module)

		const formData = new FormData();
		formData.append('file', option.file);
		formData.append('bucketName', bucketName);
		formData.append('path', path);

		let [res, error] = await to(minioApi.uploadFile(formData, {
			onUploadProgress: option.onProgress,
			signal
		}));

		if (error) res = {code: 500, message: error ?? '上传失败', data: null};
		option.onSuccess?.(res);
	}

	/**
	 * 分片上传
	 * @param option 上传选项
	 * @param signal 中断信号
	 */
	private async multipartUpload(option: IUploadOption, signal: AbortSignal) {
		const {partCount, partSize} = calculatePartSize(option.file);
		const bucketName = this.getBucketName(option.bucketType);
		const path = this.getPath(option.module)

		// 计算文件 MD5
		if (!option.file.md5) {
			option.file.md5 = await calculateFileMD5(option.file, option.onReading, signal);
		}

		// 更新进度条
		option.onProgress?.({
			percent: 0,
			loaded: 0,
			total: option.file.size
		} as UploadProgressEvent);

		const instantProgress = new InstantProgress(option.onProgress);

		try {
			// 1. 尝试秒传
			instantProgress.start(option.file.size);
			const instantResponse = await (minioApi.tryInstantUpload({
				bucketName,
				path,
				fileName: option.file.name,
				md5: option.file.md5,
			}));
			instantProgress.stop();
			if (instantResponse?.code === 200 && instantResponse.data) {
				uploadDb.remove(option.file.md5);
				option.onSuccess?.(instantResponse);
				return;
			}

			// 2. 从缓存中获取uploadID
			const uploadInfo = await uploadDb.get(option.file.md5);
			if (uploadInfo) {
				option.file.uploadId = uploadInfo?.uploadId;
			}

			// 3. 初始化分片上传
			const initData = await minioApi.initMultipartUpload({
				bucketName,
				contentType: option.file.type,
				fileName: option.file.name,
				md5: option.file.md5,
				partCount,
				path,
				uploadId: option.file.uploadId
			}, signal);

			// 4. 缓存 uploadId
			const {uploadId, uploadUrls} = initData.data;
			option.file.uploadId = uploadId;
			// 将上传信息存入缓存 如果不存在md5 或者 uploadId 不一致
			if (!uploadInfo || uploadInfo?.uploadId !== uploadId) uploadDb.put({
				md5: option.file.md5,
				uploadId: uploadId,
				timestamp: Date.now()
			});

			// 5. 上传分片
			await this.uploadTask.uploadFile({
				uploadId,
				uploadUrls,
				partSize,
				partCount,
				file: option.file,
				signal,
				onProgress: option.onProgress
			});

			// 6. 合并分片
			const mergeResponse = await (minioApi.mergeMultipartUpload({uploadId}, signal));
			option.onSuccess?.(mergeResponse);

			// 7. 删除缓存
			uploadDb.remove(option.file.md5);
		} catch (e) {
			console.error(e);
			instantProgress.stop();
			this.abort(option.file.uid);
			option.onSuccess?.({code: 500, message: '上传失败', data: null});
		}
	}

	/**
	 * 停止文件上传
	 * @param fileUid
	 */
	abort(fileUid?: number) {
		const _reqs = Object.entries(this._reqs).filter(([uid]) => String(fileUid) === uid);
		_reqs.forEach(([uid, req]) => {
			req.abort();
			delete this._reqs[uid];
		});
	}
}

/**
 * 上传进度条
 */
class InstantProgress {
	private timeoutId: number | undefined;
	private readonly callback: (event: UploadProgressEvent) => void;

	constructor(callback: (event: UploadProgressEvent) => void) {
		this.callback = callback;
	}

	start(total: number) {
		let progress = 0;

		const updateProgress = () => {
			// 更新进度的逻辑
			progress = Math.min(progress + 1, 99);

			// 调用回调函数，更新进度条
			this.callback({
				percent: progress,
				loaded: Math.round((total * progress) / 100),
				total
			} as UploadProgressEvent);

			// 生成一个 0 到 1000 毫秒之间的随机时间
			const randomDelay = Math.random() * 1000;

			// 如果进度没有达到 99.99，继续设置下一个 timeout
			if (progress < 99) {
				this.timeoutId = window.setTimeout(updateProgress, randomDelay);
			}
		};

		// 初始化进度更新
		updateProgress();
	}

	stop() {
		if (this.timeoutId) {
			clearTimeout(this.timeoutId);
		}
	}
}

const minioClient = new MiniClient();
export default minioClient;
